the new 'wrapoutputclass' parameter.
* When errorformat is not 'bc', abort reasons from action=login will be
formatted as specified by the error formatter parameters.
+* action=compare can now handle arbitrary text, deleted revisions, and
+ returning users and edit comments.
=== Action API internal changes in 1.30 ===
* …
<?php
/**
- *
- * Created on May 1, 2011
- *
- * Copyright © 2011 Sam Reed
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
class ApiComparePages extends ApiBase {
+ private $guessed = false, $guessedTitle, $guessedModel, $props;
+
public function execute() {
$params = $this->extractRequestParams();
- $rev1 = $this->revisionOrTitleOrId( $params['fromrev'], $params['fromtitle'], $params['fromid'] );
- $rev2 = $this->revisionOrTitleOrId( $params['torev'], $params['totitle'], $params['toid'] );
+ // Parameter validation
+ $this->requireAtLeastOneParameter( $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext' );
+ $this->requireAtLeastOneParameter( $params, 'totitle', 'toid', 'torev', 'totext', 'torelative' );
+
+ $this->props = array_flip( $params['prop'] );
+
+ // Cache responses publicly by default. This may be overridden later.
+ $this->getMain()->setCacheMode( 'public' );
+
+ // Get the 'from' Revision and Content
+ list( $fromRev, $fromContent, $relRev ) = $this->getDiffContent( 'from', $params );
- $revision = Revision::newFromId( $rev1 );
+ // Get the 'to' Revision and Content
+ if ( $params['torelative'] !== null ) {
+ if ( !$relRev ) {
+ $this->dieWithError( 'apierror-compare-relative-to-nothing' );
+ }
+ switch ( $params['torelative'] ) {
+ case 'prev':
+ // Swap 'from' and 'to'
+ $toRev = $fromRev;
+ $toContent = $fromContent;
+ $fromRev = $relRev->getPrevious();
+ $fromContent = $fromRev
+ ? $fromRev->getContent( Revision::FOR_THIS_USER, $this->getUser() )
+ : $toContent->getContentHandler()->makeEmptyContent();
+ if ( !$fromContent ) {
+ $this->dieWithError(
+ [ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent'
+ );
+ }
+ break;
+
+ case 'next':
+ $toRev = $relRev->getNext();
+ $toContent = $toRev
+ ? $toRev->getContent( Revision::FOR_THIS_USER, $this->getUser() )
+ : $fromContent;
+ if ( !$toContent ) {
+ $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
+ }
+ break;
+
+ case 'cur':
+ $title = $relRev->getTitle();
+ $id = $title->getLatestRevID();
+ $toRev = $id ? Revision::newFromId( $id ) : null;
+ if ( !$toRev ) {
+ $this->dieWithError(
+ [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
+ );
+ }
+ $toContent = $toRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
+ if ( !$toContent ) {
+ $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
+ }
+ break;
+ }
+ $relRev2 = null;
+ } else {
+ list( $toRev, $toContent, $relRev2 ) = $this->getDiffContent( 'to', $params );
+ }
- if ( !$revision ) {
+ // Should never happen, but just in case...
+ if ( !$fromContent || !$toContent ) {
$this->dieWithError( 'apierror-baddiff' );
}
- $contentHandler = $revision->getContentHandler();
- $de = $contentHandler->createDifferenceEngine( $this->getContext(),
- $rev1,
- $rev2,
- null, // rcid
- true,
- false );
+ // Get the diff
+ $context = new DerivativeContext( $this->getContext() );
+ if ( $relRev && $relRev->getTitle() ) {
+ $context->setTitle( $relRev->getTitle() );
+ } elseif ( $relRev2 && $relRev2->getTitle() ) {
+ $context->setTitle( $relRev2->getTitle() );
+ } else {
+ $this->guessTitleAndModel();
+ if ( $this->guessedTitle ) {
+ $context->setTitle( $this->guessedTitle );
+ }
+ }
+ $de = $fromContent->getContentHandler()->createDifferenceEngine(
+ $context,
+ $fromRev ? $fromRev->getId() : 0,
+ $toRev ? $toRev->getId() : 0,
+ /* $rcid = */ null,
+ /* $refreshCache = */ false,
+ /* $unhide = */ true
+ );
+ $de->setContent( $fromContent, $toContent );
+ $difftext = $de->getDiffBody();
+ if ( $difftext === false ) {
+ $this->dieWithError( 'apierror-baddiff' );
+ }
+ // Fill in the response
$vals = [];
- if ( isset( $params['fromtitle'] ) ) {
- $vals['fromtitle'] = $params['fromtitle'];
- }
- if ( isset( $params['fromid'] ) ) {
- $vals['fromid'] = $params['fromid'];
+ $this->setVals( $vals, 'from', $fromRev );
+ $this->setVals( $vals, 'to', $toRev );
+
+ if ( isset( $this->props['rel'] ) ) {
+ if ( $fromRev ) {
+ $rev = $fromRev->getPrevious();
+ if ( $rev ) {
+ $vals['prev'] = $rev->getId();
+ }
+ }
+ if ( $toRev ) {
+ $rev = $toRev->getNext();
+ if ( $rev ) {
+ $vals['next'] = $rev->getId();
+ }
+ }
}
- $vals['fromrevid'] = $rev1;
- if ( isset( $params['totitle'] ) ) {
- $vals['totitle'] = $params['totitle'];
+
+ if ( isset( $this->props['diffsize'] ) ) {
+ $vals['diffsize'] = strlen( $difftext );
}
- if ( isset( $params['toid'] ) ) {
- $vals['toid'] = $params['toid'];
+ if ( isset( $this->props['diff'] ) ) {
+ ApiResult::setContentValue( $vals, 'body', $difftext );
}
- $vals['torevid'] = $rev2;
- $difftext = $de->getDiffBody();
+ $this->getResult()->addValue( null, $this->getModuleName(), $vals );
+ }
- if ( $difftext === false ) {
- $this->dieWithError( 'apierror-baddiff' );
+ /**
+ * Guess an appropriate default Title and content model for this request
+ *
+ * Fills in $this->guessedTitle based on the first of 'fromrev',
+ * 'fromtitle', 'fromid', 'torev', 'totitle', and 'toid' that's present and
+ * valid.
+ *
+ * Fills in $this->guessedModel based on the Revision or Title used to
+ * determine $this->guessedTitle, or the 'fromcontentmodel' or
+ * 'tocontentmodel' parameters if no title was guessed.
+ */
+ private function guessTitleAndModel() {
+ if ( $this->guessed ) {
+ return;
}
- ApiResult::setContentValue( $vals, 'body', $difftext );
+ $this->guessed = true;
+ $params = $this->extractRequestParams();
- $this->getResult()->addValue( null, $this->getModuleName(), $vals );
+ foreach ( [ 'from', 'to' ] as $prefix ) {
+ if ( $params["{$prefix}rev"] !== null ) {
+ $revId = $params["{$prefix}rev"];
+ $rev = Revision::newFromId( $revId );
+ if ( !$rev ) {
+ // Titles of deleted revisions aren't secret, per T51088
+ $row = $this->getDB()->selectRow(
+ 'archive',
+ array_merge(
+ Revision::selectArchiveFields(),
+ [ 'ar_namespace', 'ar_title' ]
+ ),
+ [ 'ar_rev_id' => $revId ],
+ __METHOD__
+ );
+ if ( $row ) {
+ $rev = Revision::newFromArchiveRow( $row );
+ }
+ }
+ if ( $rev ) {
+ $this->guessedTitle = $rev->getTitle();
+ $this->guessedModel = $rev->getContentModel();
+ break;
+ }
+ }
+
+ if ( $params["{$prefix}title"] !== null ) {
+ $title = Title::newFromText( $params["{$prefix}title"] );
+ if ( $title && !$title->isExternal() ) {
+ $this->guessedTitle = $title;
+ break;
+ }
+ }
+
+ if ( $params["{$prefix}id"] !== null ) {
+ $title = Title::newFromID( $params["{$prefix}id"] );
+ if ( $title ) {
+ $this->guessedTitle = $title;
+ break;
+ }
+ }
+ }
+
+ if ( !$this->guessedModel ) {
+ if ( $this->guessedTitle ) {
+ $this->guessedModel = $this->guessedTitle->getContentModel();
+ } elseif ( $params['fromcontentmodel'] !== null ) {
+ $this->guessedModel = $params['fromcontentmodel'];
+ } elseif ( $params['tocontentmodel'] !== null ) {
+ $this->guessedModel = $params['tocontentmodel'];
+ }
+ }
}
/**
- * @param int $revision
- * @param string $titleText
- * @param int $titleId
- * @return int
+ * Get the Revision and Content for one side of the diff
+ *
+ * This uses the appropriate set of 'rev', 'id', 'title', 'text', 'pst',
+ * 'contentmodel', and 'contentformat' parameters to determine what content
+ * should be diffed.
+ *
+ * Returns three values:
+ * - The revision used to retrieve the content, if any
+ * - The content to be diffed
+ * - The revision specified, if any, even if not used to retrieve the
+ * Content
+ *
+ * @param string $prefix 'from' or 'to'
+ * @param array $params
+ * @return array [ Revision|null, Content, Revision|null ]
*/
- private function revisionOrTitleOrId( $revision, $titleText, $titleId ) {
- if ( $revision ) {
- return $revision;
- } elseif ( $titleText ) {
- $title = Title::newFromText( $titleText );
- if ( !$title || $title->isExternal() ) {
- $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titleText ) ] );
- }
-
- return $title->getLatestRevID();
- } elseif ( $titleId ) {
- $title = Title::newFromID( $titleId );
+ private function getDiffContent( $prefix, array $params ) {
+ $title = null;
+ $rev = null;
+ $suppliedContent = $params["{$prefix}text"] !== null;
+
+ // Get the revision and title, if applicable
+ $revId = null;
+ if ( $params["{$prefix}rev"] !== null ) {
+ $revId = $params["{$prefix}rev"];
+ } elseif ( $params["{$prefix}title"] !== null || $params["{$prefix}id"] !== null ) {
+ if ( $params["{$prefix}title"] !== null ) {
+ $title = Title::newFromText( $params["{$prefix}title"] );
+ if ( !$title || $title->isExternal() ) {
+ $this->dieWithError(
+ [ 'apierror-invalidtitle', wfEscapeWikiText( $params["{$prefix}title"] ) ]
+ );
+ }
+ } else {
+ $title = Title::newFromID( $params["{$prefix}id"] );
+ if ( !$title ) {
+ $this->dieWithError( [ 'apierror-nosuchpageid', $params["{$prefix}id"] ] );
+ }
+ }
+ $revId = $title->getLatestRevID();
+ if ( !$revId ) {
+ $revId = null;
+ // Only die here if we're not using supplied text
+ if ( !$suppliedContent ) {
+ if ( $title->exists() ) {
+ $this->dieWithError(
+ [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
+ );
+ } else {
+ $this->dieWithError(
+ [ 'apierror-missingtitle-byname', wfEscapeWikiText( $title->getPrefixedText() ) ],
+ 'missingtitle'
+ );
+ }
+ }
+ }
+ }
+ if ( $revId !== null ) {
+ $rev = Revision::newFromId( $revId );
+ if ( !$rev && $this->getUser()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
+ // Try the 'archive' table
+ $row = $this->getDB()->selectRow(
+ 'archive',
+ array_merge(
+ Revision::selectArchiveFields(),
+ [ 'ar_namespace', 'ar_title' ]
+ ),
+ [ 'ar_rev_id' => $revId ],
+ __METHOD__
+ );
+ if ( $row ) {
+ $rev = Revision::newFromArchiveRow( $row );
+ $rev->isArchive = true;
+ }
+ }
+ if ( !$rev ) {
+ $this->dieWithError( [ 'apierror-nosuchrevid', $revId ] );
+ }
+ $title = $rev->getTitle();
+
+ // If we don't have supplied content, return here. Otherwise,
+ // continue on below with the supplied content.
+ if ( !$suppliedContent ) {
+ $content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
+ if ( !$content ) {
+ $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ], 'missingcontent' );
+ }
+ return [ $rev, $content, $rev ];
+ }
+ }
+
+ // Override $content based on supplied text
+ $model = $params["{$prefix}contentmodel"];
+ $format = $params["{$prefix}contentformat"];
+
+ if ( !$model && $rev ) {
+ $model = $rev->getContentModel();
+ }
+ if ( !$model && $title ) {
+ $model = $title->getContentModel();
+ }
+ if ( !$model ) {
+ $this->guessTitleAndModel();
+ $model = $this->guessedModel;
+ }
+ if ( !$model ) {
+ $model = CONTENT_MODEL_WIKITEXT;
+ $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
+ }
+
+ if ( !$title ) {
+ $this->guessTitleAndModel();
+ $title = $this->guessedTitle;
+ }
+
+ try {
+ $content = ContentHandler::makeContent( $params["{$prefix}text"], $title, $model, $format );
+ } catch ( MWContentSerializationException $ex ) {
+ $this->dieWithException( $ex, [
+ 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
+ ] );
+ }
+
+ if ( $params["{$prefix}pst"] ) {
if ( !$title ) {
- $this->dieWithError( [ 'apierror-nosuchpageid', $titleId ] );
+ $this->dieWithError( 'apierror-compare-no-title' );
+ }
+ $popts = ParserOptions::newFromContext( $this->getContext() );
+ $content = $content->preSaveTransform( $title, $this->getUser(), $popts );
+ }
+
+ return [ null, $content, $rev ];
+ }
+
+ /**
+ * Set value fields from a Revision object
+ * @param array &$vals Result array to set data into
+ * @param string $prefix 'from' or 'to'
+ * @param Revision|null $rev
+ */
+ private function setVals( &$vals, $prefix, $rev ) {
+ if ( $rev ) {
+ $title = $rev->getTitle();
+ if ( isset( $this->props['ids'] ) ) {
+ $vals["{$prefix}id"] = $title->getArticleId();
+ $vals["{$prefix}revid"] = $rev->getId();
+ }
+ if ( isset( $this->props['title'] ) ) {
+ ApiQueryBase::addTitleInfo( $vals, $title, $prefix );
+ }
+ if ( isset( $this->props['size'] ) ) {
+ $vals["{$prefix}size"] = $rev->getSize();
}
- return $title->getLatestRevID();
+ $anyHidden = false;
+ if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $vals["{$prefix}texthidden"] = true;
+ $anyHidden = true;
+ }
+
+ if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
+ $vals["{$prefix}userhidden"] = true;
+ $anyHidden = true;
+ }
+ if ( isset( $this->props['user'] ) &&
+ $rev->userCan( Revision::DELETED_USER, $this->getUser() )
+ ) {
+ $vals["{$prefix}user"] = $rev->getUserText( Revision::RAW );
+ $vals["{$prefix}userid"] = $rev->getUser( Revision::RAW );
+ }
+
+ if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
+ $vals["{$prefix}commenthidden"] = true;
+ $anyHidden = true;
+ }
+ if ( $rev->userCan( Revision::DELETED_COMMENT, $this->getUser() ) ) {
+ if ( isset( $this->props['comment'] ) ) {
+ $vals["{$prefix}comment"] = $rev->getComment( Revision::RAW );
+ }
+ if ( isset( $this->props['parsedcomment'] ) ) {
+ $vals["{$prefix}parsedcomment"] = Linker::formatComment(
+ $rev->getComment( Revision::RAW ),
+ $rev->getTitle()
+ );
+ }
+ }
+
+ if ( $anyHidden ) {
+ $this->getMain()->setCacheMode( 'private' );
+ if ( $rev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
+ $vals["{$prefix}suppressed"] = true;
+ }
+ }
+
+ if ( !empty( $rev->isArchive ) ) {
+ $this->getMain()->setCacheMode( 'private' );
+ $vals["{$prefix}archive"] = true;
+ }
}
- $this->dieWithError( 'apierror-compare-inputneeded', 'inputneeded' );
}
public function getAllowedParams() {
- return [
- 'fromtitle' => null,
- 'fromid' => [
+ // Parameters for the 'from' and 'to' content
+ $fromToParams = [
+ 'title' => null,
+ 'id' => [
ApiBase::PARAM_TYPE => 'integer'
],
- 'fromrev' => [
+ 'rev' => [
ApiBase::PARAM_TYPE => 'integer'
],
- 'totitle' => null,
- 'toid' => [
- ApiBase::PARAM_TYPE => 'integer'
+ 'text' => [
+ ApiBase::PARAM_TYPE => 'text'
],
- 'torev' => [
- ApiBase::PARAM_TYPE => 'integer'
+ 'pst' => false,
+ 'contentformat' => [
+ ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
],
+ 'contentmodel' => [
+ ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
+ ]
];
+
+ $ret = [];
+ foreach ( $fromToParams as $k => $v ) {
+ $ret["from$k"] = $v;
+ }
+ foreach ( $fromToParams as $k => $v ) {
+ $ret["to$k"] = $v;
+ }
+
+ $ret = wfArrayInsertAfter(
+ $ret,
+ [ 'torelative' => [ ApiBase::PARAM_TYPE => [ 'prev', 'next', 'cur' ], ] ],
+ 'torev'
+ );
+
+ $ret['prop'] = [
+ ApiBase::PARAM_DFLT => 'diff|ids|title',
+ ApiBase::PARAM_TYPE => [
+ 'diff',
+ 'diffsize',
+ 'rel',
+ 'ids',
+ 'title',
+ 'user',
+ 'comment',
+ 'parsedcomment',
+ 'size',
+ ],
+ ApiBase::PARAM_ISMULTI => true,
+ ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
+ ];
+
+ return $ret;
}
protected function getExamplesMessages() {
"apihelp-clientlogin-example-login": "Start the process of logging in to the wiki as user <kbd>Example</kbd> with password <kbd>ExamplePassword</kbd>.",
"apihelp-clientlogin-example-login2": "Continue logging in after a <samp>UI</samp> response for two-factor auth, supplying an <var>OATHToken</var> of <kbd>987654</kbd>.",
- "apihelp-compare-description": "Get the difference between 2 pages.\n\nA revision number, a page title, or a page ID for both \"from\" and \"to\" must be passed.",
+ "apihelp-compare-description": "Get the difference between two pages.\n\nA revision number, a page title, a page ID, text, or a relative reference for both \"from\" and \"to\" must be passed.",
"apihelp-compare-param-fromtitle": "First title to compare.",
"apihelp-compare-param-fromid": "First page ID to compare.",
"apihelp-compare-param-fromrev": "First revision to compare.",
+ "apihelp-compare-param-fromtext": "Use this text instead of the content of the revision specified by <var>fromtitle</var>, <var>fromid</var> or <var>fromrev</var>.",
+ "apihelp-compare-param-frompst": "Do a pre-save transform on <var>fromtext</var>.",
+ "apihelp-compare-param-fromcontentmodel": "Content model of <var>fromtext</var>. If not supplied, it will be guessed based on the other parameters.",
+ "apihelp-compare-param-fromcontentformat": "Content serialization format of <var>fromtext</var>.",
"apihelp-compare-param-totitle": "Second title to compare.",
"apihelp-compare-param-toid": "Second page ID to compare.",
"apihelp-compare-param-torev": "Second revision to compare.",
+ "apihelp-compare-param-torelative": "Use a revision relative to the revision determined from <var>fromtitle</var>, <var>fromid</var> or <var>fromrev</var>. All of the other 'to' options will be ignored.",
+ "apihelp-compare-param-totext": "Use this text instead of the content of the revision specified by <var>totitle</var>, <var>toid</var> or <var>torev</var>.",
+ "apihelp-compare-param-topst": "Do a pre-save transform on <var>totext</var>.",
+ "apihelp-compare-param-tocontentmodel": "Content model of <var>totext</var>. If not supplied, it will be guessed based on the other parameters.",
+ "apihelp-compare-param-tocontentformat": "Content serialization format of <var>totext</var>.",
+ "apihelp-compare-param-prop": "Which pieces of information to get.",
+ "apihelp-compare-paramvalue-prop-diff": "The diff HTML.",
+ "apihelp-compare-paramvalue-prop-diffsize": "The size of the diff HTML, in bytes.",
+ "apihelp-compare-paramvalue-prop-rel": "The revision IDs of the revision previous to 'from' and after 'to', if any.",
+ "apihelp-compare-paramvalue-prop-ids": "The page and revision IDs of the 'from' and 'to' revisions.",
+ "apihelp-compare-paramvalue-prop-title": "The page titles of the 'from' and 'to' revisions.",
+ "apihelp-compare-paramvalue-prop-user": "The user name and ID of the 'from' and 'to' revisions.",
+ "apihelp-compare-paramvalue-prop-comment": "The comment on the 'from' and 'to' revisions.",
+ "apihelp-compare-paramvalue-prop-parsedcomment": "The parsed comment on the 'from' and 'to' revisions.",
+ "apihelp-compare-paramvalue-prop-size": "The size of the 'from' and 'to' revisions.",
"apihelp-compare-example-1": "Create a diff between revision 1 and 2.",
"apihelp-createaccount-description": "Create a new user account.",
"apierror-changeauth-norequest": "Failed to create change request.",
"apierror-chunk-too-small": "Minimum chunk size is $1 {{PLURAL:$1|byte|bytes}} for non-final chunks.",
"apierror-cidrtoobroad": "$1 CIDR ranges broader than /$2 are not accepted.",
- "apierror-compare-inputneeded": "A title, a page ID, or a revision number is needed for both the <var>from</var> and the <var>to</var> parameters.",
+ "apierror-compare-no-title": "Cannot pre-save transform without a title. Try specifying <var>fromtitle</var> or <var>totitle</var>.",
+ "apierror-compare-relative-to-nothing": "No 'from' revision for <var>torelative</var> to be relative to.",
"apierror-contentserializationexception": "Content serialization failed: $1",
"apierror-contenttoobig": "The content you supplied exceeds the article size limit of $1 {{PLURAL:$1|kilobyte|kilobytes}}.",
"apierror-copyuploadbaddomain": "Uploads by URL are not allowed from this domain.",
"apierror-maxlag": "Waiting for $2: $1 {{PLURAL:$1|second|seconds}} lagged.",
"apierror-mimesearchdisabled": "MIME search is disabled in Miser Mode.",
"apierror-missingcontent-pageid": "Missing content for page ID $1.",
+ "apierror-missingcontent-revid": "Missing content for revision ID $1.",
"apierror-missingparam-at-least-one-of": "{{PLURAL:$2|The parameter|At least one of the parameters}} $1 is required.",
"apierror-missingparam-one-of": "{{PLURAL:$2|The parameter|One of the parameters}} $1 is required.",
"apierror-missingparam": "The <var>$1</var> parameter must be set.",
"apierror-missingrev-pageid": "No current revision of page ID $1.",
+ "apierror-missingrev-title": "No current revision of title $1.",
"apierror-missingtitle-createonly": "Missing titles can only be protected with <kbd>create</kbd>.",
"apierror-missingtitle": "The page you specified doesn't exist.",
"apierror-missingtitle-byname": "The page $1 doesn't exist.",
"apiwarn-badurlparam": "Could not parse <var>$1urlparam</var> for $2. Using only width and height.",
"apiwarn-badutf8": "The value passed for <var>$1</var> contains invalid or non-normalized data. Textual data should be valid, NFC-normalized Unicode without C0 control characters other than HT (\\t), LF (\\n), and CR (\\r).",
"apiwarn-checktoken-percentencoding": "Check that symbols such as \"+\" in the token are properly percent-encoded in the URL.",
+ "apiwarn-compare-nocontentmodel": "No content model could be determined, assuming $1.",
"apiwarn-deprecation-deletedrevs": "<kbd>list=deletedrevs</kbd> has been deprecated. Please use <kbd>prop=deletedrevisions</kbd> or <kbd>list=alldeletedrevisions</kbd> instead.",
"apiwarn-deprecation-expandtemplates-prop": "Because no values have been specified for the <var>prop</var> parameter, a legacy format has been used for the output. This format is deprecated, and in the future, a default value will be set for the <var>prop</var> parameter, causing the new format to always be used.",
"apiwarn-deprecation-httpsexpected": "HTTP used when HTTPS was expected.",
"apihelp-clientlogin-example-login": "{{doc-apihelp-example|clientlogin}}",
"apihelp-clientlogin-example-login2": "{{doc-apihelp-example|clientlogin}}",
"apihelp-compare-description": "{{doc-apihelp-description|compare}}",
- "apihelp-compare-param-fromtitle": "{{doc-apihelp-param|compare|fromtitle}}",
+ "apihelp-compare-param-fromcontentformat": "{{doc-apihelp-param|compare|fromcontentformat}}",
+ "apihelp-compare-param-fromcontentmodel": "{{doc-apihelp-param|compare|fromcontentmodel}}",
"apihelp-compare-param-fromid": "{{doc-apihelp-param|compare|fromid}}",
+ "apihelp-compare-param-frompst": "{{doc-apihelp-param|compare|frompst}}",
"apihelp-compare-param-fromrev": "{{doc-apihelp-param|compare|fromrev}}",
- "apihelp-compare-param-totitle": "{{doc-apihelp-param|compare|totitle}}",
+ "apihelp-compare-param-fromtext": "{{doc-apihelp-param|compare|fromtext}}",
+ "apihelp-compare-param-fromtitle": "{{doc-apihelp-param|compare|fromtitle}}",
+ "apihelp-compare-param-prop": "{{doc-apihelp-param|compare|prop}}",
+ "apihelp-compare-param-tocontentformat": "{{doc-apihelp-param|compare|tocontentformat}}",
+ "apihelp-compare-param-tocontentmodel": "{{doc-apihelp-param|compare|tocontentmodel}}",
"apihelp-compare-param-toid": "{{doc-apihelp-param|compare|toid}}",
+ "apihelp-compare-param-topst": "{{doc-apihelp-param|compare|topst}}",
+ "apihelp-compare-param-torelative": "{{doc-apihelp-param|compare|torelative}}",
"apihelp-compare-param-torev": "{{doc-apihelp-param|compare|torev}}",
+ "apihelp-compare-param-totext": "{{doc-apihelp-param|compare|totext}}",
+ "apihelp-compare-param-totitle": "{{doc-apihelp-param|compare|totitle}}",
+ "apihelp-compare-paramvalue-prop-comment": "{{doc-apihelp-paramvalue|compare|prop|comment}}",
+ "apihelp-compare-paramvalue-prop-diff": "{{doc-apihelp-paramvalue|compare|prop|diff}}",
+ "apihelp-compare-paramvalue-prop-diffsize": "{{doc-apihelp-paramvalue|compare|prop|diffsize}}",
+ "apihelp-compare-paramvalue-prop-ids": "{{doc-apihelp-paramvalue|compare|prop|ids}}",
+ "apihelp-compare-paramvalue-prop-parsedcomment": "{{doc-apihelp-paramvalue|compare|prop|parsedcomment}}",
+ "apihelp-compare-paramvalue-prop-rel": "{{doc-apihelp-paramvalue|compare|prop|rel}}",
+ "apihelp-compare-paramvalue-prop-size": "{{doc-apihelp-paramvalue|compare|prop|size}}",
+ "apihelp-compare-paramvalue-prop-title": "{{doc-apihelp-paramvalue|compare|prop|title}}",
+ "apihelp-compare-paramvalue-prop-user": "{{doc-apihelp-paramvalue|compare|prop|user}}",
"apihelp-compare-example-1": "{{doc-apihelp-example|compare}}",
"apihelp-createaccount-description": "{{doc-apihelp-description|createaccount}}",
"apihelp-createaccount-param-preservestate": "{{doc-apihelp-param|createaccount|preservestate|info=This message is displayed in addition to {{msg-mw|api-help-authmanagerhelper-preservestate}}.}}",
"apierror-changeauth-norequest": "{{doc-apierror}}",
"apierror-chunk-too-small": "{{doc-apierror}}\n\nParameters:\n* $1 - Minimum size in bytes.",
"apierror-cidrtoobroad": "{{doc-apierror}}\n\nParameters:\n* $1 - \"IPv4\" or \"IPv6\"\n* $2 - Minimum CIDR mask length.",
- "apierror-compare-inputneeded": "{{doc-apierror}}",
+ "apierror-compare-no-title": "{{doc-apierror}}",
+ "apierror-compare-relative-to-nothing": "{{doc-apierror}}",
"apierror-contentserializationexception": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, may end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.",
"apierror-contenttoobig": "{{doc-apierror}}\n\nParameters:\n* $1 - Maximum article size in kilobytes.",
"apierror-copyuploadbaddomain": "{{doc-apierror}}",
"apierror-maxlag": "{{doc-apierror}}\n\nParameters:\n* $1 - Database lag in seconds.\n* $2 - Database server that is lagged.",
"apierror-mimesearchdisabled": "{{doc-apierror}}",
"apierror-missingcontent-pageid": "{{doc-apierror}}\n\nParameters:\n* $1 - Page ID number.",
+ "apierror-missingcontent-revid": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number",
"apierror-missingparam-at-least-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names.\n* $2 - Number of parameters.",
"apierror-missingparam-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names.\n* $2 - Number of parameters.",
"apierror-missingparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
"apierror-missingrev-pageid": "{{doc-apierror}}\n\nParameters:\n* $1 - Page ID number.",
+ "apierror-missingrev-title": "{{doc-apierror}}\n\nParameters:\n* $1 - Page title.",
"apierror-missingtitle-createonly": "{{doc-apierror}}",
"apierror-missingtitle": "{{doc-apierror}}",
"apierror-missingtitle-byname": "{{doc-apierror}}",
"apiwarn-badurlparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".\n* $2 - Image title.",
"apiwarn-badutf8": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n{{doc-important|Do not translate \"\\t\", \"\\n\", and \"\\r\"}}",
"apiwarn-checktoken-percentencoding": "{{doc-apierror}}",
+ "apiwarn-compare-nocontentmodel": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model being assumed.",
"apiwarn-deprecation-deletedrevs": "{{doc-apierror}}",
"apiwarn-deprecation-expandtemplates-prop": "{{doc-apierror}}",
"apiwarn-deprecation-httpsexpected": "{{doc-apierror}}",
--- /dev/null
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ * @covers ApiComparePages
+ */
+class ApiComparePagesTest extends ApiTestCase {
+
+ protected static $repl = [];
+
+ protected function setUp() {
+ parent::setUp();
+
+ // Set $wgExternalDiffEngine to something bogus to try to force use of
+ // the PHP engine rather than wikidiff2.
+ $this->setMwGlobals( [
+ 'wgExternalDiffEngine' => '/dev/null',
+ ] );
+ }
+
+ protected function addPage( $page, $text, $model = CONTENT_MODEL_WIKITEXT ) {
+ $title = Title::newFromText( 'ApiComparePagesTest ' . $page );
+ $content = ContentHandler::makeContent( $text, $title, $model );
+
+ $page = WikiPage::factory( $title );
+ $user = static::getTestSysop()->getUser();
+ $status = $page->doEditContent(
+ $content, 'Test for ApiComparePagesTest: ' . $text, 0, false, $user
+ );
+ if ( !$status->isOk() ) {
+ $this->fail( "Failed to create $title: " . $status->getWikiText( false, false, 'en' ) );
+ }
+ return $status->value['revision']->getId();
+ }
+
+ public function addDBDataOnce() {
+ $user = static::getTestSysop()->getUser();
+ self::$repl['creator'] = $user->getName();
+ self::$repl['creatorid'] = $user->getId();
+
+ self::$repl['revA1'] = $this->addPage( 'A', 'A 1' );
+ self::$repl['revA2'] = $this->addPage( 'A', 'A 2' );
+ self::$repl['revA3'] = $this->addPage( 'A', 'A 3' );
+ self::$repl['revA4'] = $this->addPage( 'A', 'A 4' );
+ self::$repl['pageA'] = Title::newFromText( 'ApiComparePagesTest A' )->getArticleId();
+
+ self::$repl['revB1'] = $this->addPage( 'B', 'B 1' );
+ self::$repl['revB2'] = $this->addPage( 'B', 'B 2' );
+ self::$repl['revB3'] = $this->addPage( 'B', 'B 3' );
+ self::$repl['revB4'] = $this->addPage( 'B', 'B 4' );
+ self::$repl['pageB'] = Title::newFromText( 'ApiComparePagesTest B' )->getArticleId();
+
+ self::$repl['revC1'] = $this->addPage( 'C', 'C 1' );
+ self::$repl['revC2'] = $this->addPage( 'C', 'C 2' );
+ self::$repl['revC3'] = $this->addPage( 'C', 'C 3' );
+ self::$repl['pageC'] = Title::newFromText( 'ApiComparePagesTest C' )->getArticleId();
+
+ $id = $this->addPage( 'D', 'D 1' );
+ self::$repl['pageD'] = Title::newFromText( 'ApiComparePagesTest D' )->getArticleId();
+ wfGetDB( DB_MASTER )->delete( 'revision', [ 'rev_id' => $id ] );
+
+ self::$repl['revE1'] = $this->addPage( 'E', 'E 1' );
+ self::$repl['revE2'] = $this->addPage( 'E', 'E 2' );
+ self::$repl['revE3'] = $this->addPage( 'E', 'E 3' );
+ self::$repl['revE4'] = $this->addPage( 'E', 'E 4' );
+ self::$repl['pageE'] = Title::newFromText( 'ApiComparePagesTest E' )->getArticleId();
+ wfGetDB( DB_MASTER )->update(
+ 'page', [ 'page_latest' => 0 ], [ 'page_id' => self::$repl['pageE'] ]
+ );
+
+ WikiPage::factory( Title::newFromText( 'ApiComparePagesTest C' ) )
+ ->doDeleteArticleReal( 'Test for ApiComparePagesTest' );
+
+ RevisionDeleter::createList(
+ 'revision',
+ RequestContext::getMain(),
+ Title::newFromText( 'ApiComparePagesTest B' ),
+ [ self::$repl['revB2'] ]
+ )->setVisibility( [
+ 'value' => [
+ Revision::DELETED_TEXT => 1,
+ Revision::DELETED_USER => 1,
+ Revision::DELETED_COMMENT => 1,
+ ],
+ 'comment' => 'Test for ApiComparePages',
+ ] );
+
+ RevisionDeleter::createList(
+ 'revision',
+ RequestContext::getMain(),
+ Title::newFromText( 'ApiComparePagesTest B' ),
+ [ self::$repl['revB3'] ]
+ )->setVisibility( [
+ 'value' => [
+ Revision::DELETED_USER => 1,
+ Revision::DELETED_COMMENT => 1,
+ Revision::DELETED_RESTRICTED => 1,
+ ],
+ 'comment' => 'Test for ApiComparePages',
+ ] );
+
+ Title::clearCaches(); // Otherwise it has the wrong latest revision for some reason
+ }
+
+ protected function doReplacements( &$value ) {
+ if ( is_string( $value ) ) {
+ if ( preg_match( '/^{{REPL:(.+?)}}$/', $value, $m ) ) {
+ $value = self::$repl[$m[1]];
+ } else {
+ $value = preg_replace_callback( '/{{REPL:(.+?)}}/', function ( $m ) {
+ return isset( self::$repl[$m[1]] ) ? self::$repl[$m[1]] : $m[0];
+ }, $value );
+ }
+ } elseif ( is_array( $value ) || is_object( $value ) ) {
+ foreach ( $value as &$v ) {
+ $this->doReplacements( $v );
+ }
+ unset( $v );
+ }
+ }
+
+ /**
+ * @dataProvider provideDiff
+ */
+ public function testDiff( $params, $expect, $exceptionCode = false, $sysop = false ) {
+ $this->doReplacements( $params );
+
+ $params += [
+ 'action' => 'compare',
+ ];
+
+ $user = $sysop
+ ? static::getTestSysop()->getUser()
+ : static::getTestUser()->getUser();
+ if ( $exceptionCode ) {
+ try {
+ $this->doApiRequest( $params, null, false, $user );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( ApiUsageException $ex ) {
+ $this->assertTrue( $this->apiExceptionHasCode( $ex, $exceptionCode ),
+ "Exception with code $exceptionCode" );
+ }
+ } else {
+ $apiResult = $this->doApiRequest( $params, null, false, $user );
+ $apiResult = $apiResult[0];
+ $this->doReplacements( $expect );
+ $this->assertEquals( $expect, $apiResult );
+ }
+ }
+
+ public static function provideDiff() {
+ return [
+ // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
+ 'Basic diff, titles' => [
+ [
+ 'fromtitle' => 'ApiComparePagesTest A',
+ 'totitle' => 'ApiComparePagesTest B',
+ ],
+ [
+ 'compare' => [
+ 'fromid' => '{{REPL:pageA}}',
+ 'fromrevid' => '{{REPL:revA4}}',
+ 'fromns' => 0,
+ 'fromtitle' => 'ApiComparePagesTest A',
+ 'toid' => '{{REPL:pageB}}',
+ 'torevid' => '{{REPL:revB4}}',
+ 'tons' => 0,
+ 'totitle' => 'ApiComparePagesTest B',
+ 'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+ . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+ . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">A </del>4</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">B </ins>4</div></td></tr>' . "\n",
+ ]
+ ],
+ ],
+ 'Basic diff, page IDs' => [
+ [
+ 'fromid' => '{{REPL:pageA}}',
+ 'toid' => '{{REPL:pageB}}',
+ ],
+ [
+ 'compare' => [
+ 'fromid' => '{{REPL:pageA}}',
+ 'fromrevid' => '{{REPL:revA4}}',
+ 'fromns' => 0,
+ 'fromtitle' => 'ApiComparePagesTest A',
+ 'toid' => '{{REPL:pageB}}',
+ 'torevid' => '{{REPL:revB4}}',
+ 'tons' => 0,
+ 'totitle' => 'ApiComparePagesTest B',
+ 'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+ . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+ . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">A </del>4</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">B </ins>4</div></td></tr>' . "\n",
+ ]
+ ],
+ ],
+ 'Basic diff, revision IDs' => [
+ [
+ 'fromrev' => '{{REPL:revA2}}',
+ 'torev' => '{{REPL:revA3}}',
+ ],
+ [
+ 'compare' => [
+ 'fromid' => '{{REPL:pageA}}',
+ 'fromrevid' => '{{REPL:revA2}}',
+ 'fromns' => 0,
+ 'fromtitle' => 'ApiComparePagesTest A',
+ 'toid' => '{{REPL:pageA}}',
+ 'torevid' => '{{REPL:revA3}}',
+ 'tons' => 0,
+ 'totitle' => 'ApiComparePagesTest A',
+ 'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+ . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+ . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div>A <del class="diffchange diffchange-inline">2</del></div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div>A <ins class="diffchange diffchange-inline">3</ins></div></td></tr>' . "\n",
+ ]
+ ],
+ ],
+ 'Basic diff, deleted revision ID as sysop' => [
+ [
+ 'fromrev' => '{{REPL:revA2}}',
+ 'torev' => '{{REPL:revC2}}',
+ ],
+ [
+ 'compare' => [
+ 'fromid' => '{{REPL:pageA}}',
+ 'fromrevid' => '{{REPL:revA2}}',
+ 'fromns' => 0,
+ 'fromtitle' => 'ApiComparePagesTest A',
+ 'toid' => 0,
+ 'torevid' => '{{REPL:revC2}}',
+ 'tons' => 0,
+ 'totitle' => 'ApiComparePagesTest C',
+ 'toarchive' => true,
+ 'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+ . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+ . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">A </del>2</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">C </ins>2</div></td></tr>' . "\n",
+ ]
+ ],
+ false, true
+ ],
+ 'Basic diff, revdel as sysop' => [
+ [
+ 'fromrev' => '{{REPL:revA2}}',
+ 'torev' => '{{REPL:revB2}}',
+ ],
+ [
+ 'compare' => [
+ 'fromid' => '{{REPL:pageA}}',
+ 'fromrevid' => '{{REPL:revA2}}',
+ 'fromns' => 0,
+ 'fromtitle' => 'ApiComparePagesTest A',
+ 'toid' => '{{REPL:pageB}}',
+ 'torevid' => '{{REPL:revB2}}',
+ 'tons' => 0,
+ 'totitle' => 'ApiComparePagesTest B',
+ 'totexthidden' => true,
+ 'touserhidden' => true,
+ 'tocommenthidden' => true,
+ 'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+ . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+ . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">A </del>2</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">B </ins>2</div></td></tr>' . "\n",
+ ]
+ ],
+ false, true
+ ],
+ 'Basic diff, text' => [
+ [
+ 'fromtext' => 'From text',
+ 'fromcontentmodel' => 'wikitext',
+ 'totext' => 'To text {{subst:PAGENAME}}',
+ 'tocontentmodel' => 'wikitext',
+ ],
+ [
+ 'compare' => [
+ 'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+ . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+ . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">From </del>text</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To </ins>text <ins class="diffchange diffchange-inline">{{subst:PAGENAME}}</ins></div></td></tr>' . "\n",
+ ]
+ ],
+ ],
+ 'Basic diff, text 2' => [
+ [
+ 'fromtext' => 'From text',
+ 'totext' => 'To text {{subst:PAGENAME}}',
+ 'tocontentmodel' => 'wikitext',
+ ],
+ [
+ 'compare' => [
+ 'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+ . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+ . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">From </del>text</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To </ins>text <ins class="diffchange diffchange-inline">{{subst:PAGENAME}}</ins></div></td></tr>' . "\n",
+ ]
+ ],
+ ],
+ 'Basic diff, guessed model' => [
+ [
+ 'fromtext' => 'From text',
+ 'totext' => 'To text',
+ ],
+ [
+ 'warnings' => [
+ 'compare' => [
+ 'warnings' => 'No content model could be determined, assuming wikitext.',
+ ],
+ ],
+ 'compare' => [
+ 'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+ . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+ . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">From </del>text</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To </ins>text</div></td></tr>' . "\n",
+ ]
+ ],
+ ],
+ 'Basic diff, text with title and PST' => [
+ [
+ 'fromtext' => 'From text',
+ 'totitle' => 'Test',
+ 'totext' => 'To text {{subst:PAGENAME}}',
+ 'topst' => true,
+ ],
+ [
+ 'compare' => [
+ 'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+ . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+ . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">From </del>text</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To </ins>text <ins class="diffchange diffchange-inline">Test</ins></div></td></tr>' . "\n",
+ ]
+ ],
+ ],
+ 'Basic diff, text with page ID and PST' => [
+ [
+ 'fromtext' => 'From text',
+ 'toid' => '{{REPL:pageB}}',
+ 'totext' => 'To text {{subst:PAGENAME}}',
+ 'topst' => true,
+ ],
+ [
+ 'compare' => [
+ 'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+ . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+ . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">From </del>text</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To </ins>text <ins class="diffchange diffchange-inline">ApiComparePagesTest B</ins></div></td></tr>' . "\n",
+ ]
+ ],
+ ],
+ 'Basic diff, text with revision and PST' => [
+ [
+ 'fromtext' => 'From text',
+ 'torev' => '{{REPL:revB2}}',
+ 'totext' => 'To text {{subst:PAGENAME}}',
+ 'topst' => true,
+ ],
+ [
+ 'compare' => [
+ 'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+ . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+ . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">From </del>text</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To </ins>text <ins class="diffchange diffchange-inline">ApiComparePagesTest B</ins></div></td></tr>' . "\n",
+ ]
+ ],
+ ],
+ 'Basic diff, text with deleted revision and PST' => [
+ [
+ 'fromtext' => 'From text',
+ 'torev' => '{{REPL:revC2}}',
+ 'totext' => 'To text {{subst:PAGENAME}}',
+ 'topst' => true,
+ ],
+ [
+ 'compare' => [
+ 'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+ . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+ . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">From </del>text</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To </ins>text <ins class="diffchange diffchange-inline">ApiComparePagesTest C</ins></div></td></tr>' . "\n",
+ ]
+ ],
+ false, true
+ ],
+ 'Diff with all props' => [
+ [
+ 'fromrev' => '{{REPL:revB1}}',
+ 'torev' => '{{REPL:revB3}}',
+ 'totitle' => 'ApiComparePagesTest B',
+ 'prop' => 'diff|diffsize|rel|ids|title|user|comment|parsedcomment|size'
+ ],
+ [
+ 'compare' => [
+ 'fromid' => '{{REPL:pageB}}',
+ 'fromrevid' => '{{REPL:revB1}}',
+ 'fromns' => 0,
+ 'fromtitle' => 'ApiComparePagesTest B',
+ 'fromsize' => 3,
+ 'fromuser' => '{{REPL:creator}}',
+ 'fromuserid' => '{{REPL:creatorid}}',
+ 'fromcomment' => 'Test for ApiComparePagesTest: B 1',
+ 'fromparsedcomment' => 'Test for ApiComparePagesTest: B 1',
+ 'toid' => '{{REPL:pageB}}',
+ 'torevid' => '{{REPL:revB3}}',
+ 'tons' => 0,
+ 'totitle' => 'ApiComparePagesTest B',
+ 'tosize' => 3,
+ 'touserhidden' => true,
+ 'tocommenthidden' => true,
+ 'tosuppressed' => true,
+ 'next' => '{{REPL:revB4}}',
+ 'diffsize' => 391,
+ 'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+ . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+ . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div>B <del class="diffchange diffchange-inline">1</del></div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div>B <ins class="diffchange diffchange-inline">3</ins></div></td></tr>' . "\n",
+ ]
+ ],
+ ],
+ 'Diff with all props as sysop' => [
+ [
+ 'fromrev' => '{{REPL:revB2}}',
+ 'torev' => '{{REPL:revB3}}',
+ 'totitle' => 'ApiComparePagesTest B',
+ 'prop' => 'diff|diffsize|rel|ids|title|user|comment|parsedcomment|size'
+ ],
+ [
+ 'compare' => [
+ 'fromid' => '{{REPL:pageB}}',
+ 'fromrevid' => '{{REPL:revB2}}',
+ 'fromns' => 0,
+ 'fromtitle' => 'ApiComparePagesTest B',
+ 'fromsize' => 3,
+ 'fromtexthidden' => true,
+ 'fromuserhidden' => true,
+ 'fromuser' => '{{REPL:creator}}',
+ 'fromuserid' => '{{REPL:creatorid}}',
+ 'fromcommenthidden' => true,
+ 'fromcomment' => 'Test for ApiComparePagesTest: B 2',
+ 'fromparsedcomment' => 'Test for ApiComparePagesTest: B 2',
+ 'toid' => '{{REPL:pageB}}',
+ 'torevid' => '{{REPL:revB3}}',
+ 'tons' => 0,
+ 'totitle' => 'ApiComparePagesTest B',
+ 'tosize' => 3,
+ 'touserhidden' => true,
+ 'tocommenthidden' => true,
+ 'tosuppressed' => true,
+ 'prev' => '{{REPL:revB1}}',
+ 'next' => '{{REPL:revB4}}',
+ 'diffsize' => 391,
+ 'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+ . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+ . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div>B <del class="diffchange diffchange-inline">2</del></div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div>B <ins class="diffchange diffchange-inline">3</ins></div></td></tr>' . "\n",
+ ]
+ ],
+ false, true
+ ],
+ 'Relative diff, cur' => [
+ [
+ 'fromrev' => '{{REPL:revA2}}',
+ 'torelative' => 'cur',
+ 'prop' => 'ids',
+ ],
+ [
+ 'compare' => [
+ 'fromid' => '{{REPL:pageA}}',
+ 'fromrevid' => '{{REPL:revA2}}',
+ 'toid' => '{{REPL:pageA}}',
+ 'torevid' => '{{REPL:revA4}}',
+ ]
+ ],
+ ],
+ 'Relative diff, next' => [
+ [
+ 'fromrev' => '{{REPL:revE2}}',
+ 'torelative' => 'next',
+ 'prop' => 'ids|rel',
+ ],
+ [
+ 'compare' => [
+ 'fromid' => '{{REPL:pageE}}',
+ 'fromrevid' => '{{REPL:revE2}}',
+ 'toid' => '{{REPL:pageE}}',
+ 'torevid' => '{{REPL:revE3}}',
+ 'prev' => '{{REPL:revE1}}',
+ 'next' => '{{REPL:revE4}}',
+ ]
+ ],
+ ],
+ 'Relative diff, prev' => [
+ [
+ 'fromrev' => '{{REPL:revE3}}',
+ 'torelative' => 'prev',
+ 'prop' => 'ids|rel',
+ ],
+ [
+ 'compare' => [
+ 'fromid' => '{{REPL:pageE}}',
+ 'fromrevid' => '{{REPL:revE2}}',
+ 'toid' => '{{REPL:pageE}}',
+ 'torevid' => '{{REPL:revE3}}',
+ 'prev' => '{{REPL:revE1}}',
+ 'next' => '{{REPL:revE4}}',
+ ]
+ ],
+ ],
+
+ 'Error, missing title' => [
+ [
+ 'fromtitle' => 'ApiComparePagesTest X',
+ 'totitle' => 'ApiComparePagesTest B',
+ ],
+ [],
+ 'missingtitle',
+ ],
+ 'Error, invalid title' => [
+ [
+ 'fromtitle' => '<bad>',
+ 'totitle' => 'ApiComparePagesTest B',
+ ],
+ [],
+ 'invalidtitle',
+ ],
+ 'Error, missing page ID' => [
+ [
+ 'fromid' => 8817900,
+ 'totitle' => 'ApiComparePagesTest B',
+ ],
+ [],
+ 'nosuchpageid',
+ ],
+ 'Error, page with missing revision' => [
+ [
+ 'fromtitle' => 'ApiComparePagesTest D',
+ 'totitle' => 'ApiComparePagesTest B',
+ ],
+ [],
+ 'nosuchrevid',
+ ],
+ 'Error, page with no revision' => [
+ [
+ 'fromtitle' => 'ApiComparePagesTest E',
+ 'totitle' => 'ApiComparePagesTest B',
+ ],
+ [],
+ 'nosuchrevid',
+ ],
+ 'Error, bad rev ID' => [
+ [
+ 'fromrev' => 8817900,
+ 'totitle' => 'ApiComparePagesTest B',
+ ],
+ [],
+ 'nosuchrevid',
+ ],
+ 'Error, deleted revision ID, non-sysop' => [
+ [
+ 'fromrev' => '{{REPL:revA2}}',
+ 'torev' => '{{REPL:revC2}}',
+ ],
+ [],
+ 'nosuchrevid',
+ ],
+ 'Error, revision-deleted content' => [
+ [
+ 'fromrev' => '{{REPL:revA2}}',
+ 'torev' => '{{REPL:revB2}}',
+ ],
+ [],
+ 'missingcontent',
+ ],
+ 'Error, text with no title and PST' => [
+ [
+ 'fromtext' => 'From text',
+ 'totext' => 'To text {{subst:PAGENAME}}',
+ 'topst' => true,
+ ],
+ [],
+ 'compare-no-title',
+ ],
+ 'Error, Relative diff, no from revision' => [
+ [
+ 'fromtext' => 'Foo',
+ 'torelative' => 'cur',
+ 'prop' => 'ids',
+ ],
+ [],
+ 'compare-relative-to-nothing'
+ ],
+ 'Error, Relative diff, cur with no current revision' => [
+ [
+ 'fromrev' => '{{REPL:revE2}}',
+ 'torelative' => 'cur',
+ 'prop' => 'ids',
+ ],
+ [],
+ 'nosuchrevid'
+ ],
+ 'Error, Relative diff, next revdeleted' => [
+ [
+ 'fromrev' => '{{REPL:revB1}}',
+ 'torelative' => 'next',
+ 'prop' => 'ids',
+ ],
+ [],
+ 'missingcontent'
+ ],
+ 'Error, Relative diff, prev revdeleted' => [
+ [
+ 'fromrev' => '{{REPL:revB3}}',
+ 'torelative' => 'prev',
+ 'prop' => 'ids',
+ ],
+ [],
+ 'missingcontent'
+ ],
+
+ // @codingStandardsIgnoreEnd
+ ];
+ }
+}